<?php
/**
 * 程序：验证码图像产生类
 * 版本：1.0.0
 * 著者：陈良群（Daniel Chan）
 * 联系：chenliangqun@gmail.com
 * 版权：Copyright (C) 2012, Daniel Chan (陈良群) All Rights Reserved
 * 发布：2012年03月10日
 * 协议：遵从MIT许可协议
 */

class CCaptcha
{
    /**
     * 验证码类型[const]
     * 
     * LETTER：字母
     * NUMBER：数字
     * BOTH  ：字母和数字混合
     */
    const LETTER = 1;
    const NUMBER = 2;
    const BOTH   = 4;
    
    /**
     * 干扰素类型[const]
     * 
     * BROKEN：折线
     * GRID  ：网格
     */
    const BROKEN = 1;
    const GRID   = 2;
    
    /**
     * 图像输出格式[const]
     * 
     * GIF ：gif格式
     * JPEG：jpeg格式
     * JPG ：jpg格式
     * PNG ：png格式
     */
    const GIF  = 1;
    const JPEG = 2;
    const JPG  = 2;
    const PNG  = 4;
    
    /**
     * 图像宽度[integer]
     * 
     * 由$_default['width']初始化
     */
    private $_width;
    
    /**
     * 图像高度[integer]
     * 
     * 由$_default['height']初始化
     */
    private $_height;
    
    /**
     * 图像资源[resource]
     */
    private $_image;
    
    /**
     * 图像背景颜色[string]
     * 
     * 颜色值形式为'#ffffff'或'ffffff'
     * 由$_default['background']初始化
     */
    private $_background;
    
    /**
     * 图像边框大小[integer]
     * 
     * 由$_default['border']初始化
     */
    private $_border;
    
    /**
     * 图像边框颜色[string]
     * 
     * 颜色值形式为'#ffffff'或'ffffff'
     * 由$_default['borderColor']初始化
     */
    private $_borderColor;
    
    /**
     * 干扰素类型[integer]
     * 
     * 值为CCaptcha::BROKEN或CCaptcha::GRID
     * 由$_default['lineType']初始化
     */
    private $_lineType;
    
    /**
     * 折线数量[integer]
     * 
     * 由$_default['lineCount']初始化
     */
    private $_lineCount;
    
    /**
     * 折线颜色颜色[array]
     * 
     * 颜色值形式为'#ffffff'或'ffffff'
     * 有多个值时则随机分配颜色
     * 由$_default['brokenColor']初始化
     */
    private $_brokenColor;
    
    /**
     * 网格正方形边长[integer]
     * 
     * 由$_default['gridLength']初始化
     */
    private $_gridLength;
    
    /**
     * 网格边框颜色[array]
     * 
     * 颜色值形式为'#ffffff'或'ffffff'
     * 由$_default['gridColor']初始化
     */
    private $_gridColor;
    
    /**
     * 干扰元素透明度[integer]
     * 
     * 值范围：0 - 127
     * 值为0时则完全不透明，值为127时则完全透明
     * 由$_default['transparent']初始化
     */
    private $_transparent;
    
    /**
     * 验证码字符个数[integer]
     * 
     * 由$_default['length']初始化
     */
    private $_length;
    
    /**
     * 验证码字符串[string]
     */
    private $_captcha;
    
    /**
     * 验证码字符类型[integer]
     * 
     * 值为CCaptcha::LETTER、CCaptcha::NUMBER或CCaptcha::BOTH
     * 由$_default['captchaType']初始化
     */
    private $_captchaType;
    
    /**
     * 验证码字符元素[array]
     * 
     * 颜色值形式为'#ffffff'或'ffffff'
     * 有多个值时则随机分配各个字符的颜色
     * 由$_default['color']初始化
     */
    private $_color;
    
    /**
     * 验证码字符字体[array]
     * 
     * 有多个值时则随机分配各个字符的字体
     * 由$_default['font']初始化
     */
    private $_font;
    
    /**
     * 验证码字符字体大小[array]
     * 
     * 形式为array('min' => $value, 'max' => $value)
     * 随机分配字体大小，其范围在min - max之间
     * 由$_default['fontSize']初始化
     */
    private $_fontSize;
    
    /**
     * 验证码字符旋转角度[array]
     * 
     * 形式为array('min' => $value, 'max' => $value)
     * 随机旋转各个字符，其角度范围在min - max之间
     * 由$_default['angle']初始化
     */
    private $_angle;
    
    /**
     * 图像输出格式[integer]
     * 
     * 值为CCaptcha::GIF、CCaptcha::JPEG、CCaptcha::JPG或CCaptcha::PNG
     * 由$_default['imageType']初始化
     */
    private $_imageType;
    
    /**
     * 验证码字符保存到session中的索引[string]
     * 
     * 由$_default['sessionId']初始化
     */
    private $_sessionId;
    
    /**
     * 程序版本号[string:static]
     */
    private static $_version = '1.0.0';
    
    /**
     * 默认配置值[array]
     */
    private $_default = array(
        'width'       => 180,
        'height'      => 58,
        'background'  => '#f8f8f8',
        'border'      => 1,
        'borderColor' => '#000000',
        'lineType'    => self::GRID,
        'gridLength'  => 8,
        'lineCount'   => 5,
        'brokenColor' => array(),
        'gridColor'   => array(),
        'transparent' => 99,
        'captchaType' => self::BOTH,
        'length'      => 6,
        'color'       => array('#bf0000', '#1da009', '#0a4faa', '#1c1b1a'),
        'font'        => array(),
        'fontSize'    => array('min' => 20, 'max' => 25),
        'angle'       => array('min' => -20, 'max' => 20),
        'imageType'   => self::PNG,
        'sessionId'   => 'captcha'
    );
    
    /**
     * 构造方法[public]
     * 
     * 初始化各个属性值
     * 创建图像资源
     * 填充背景颜色
     * 产生验证码字符串
     * 在图像上绘画验证码字符串
     * 绘画干扰元素
     * 绘画图像边框
     * 输入图像
     * 设置session
     * 
     * 注意：设置session必须在图像输出之后设置，否则图像无法输出
     */
    public function __construct(array $config = array())
    {        
        $this -> initialize($config)
              -> createImage()
              -> fillBackground()
              -> generateCaptcha()
              -> drawCaptcha()
              -> drawLines()
              -> drawBorder()
              -> outputImage()
              -> setSession();
    }
    
    /**
     * 初始化方法[private]
     * 
     * $config[array]：配置数组
     * 
     * 返回值[object]：对象本身
     */
    private function initialize(array $config = array())
    {
        // 判断是否开启GD库
        extension_loaded('gd') or die('This program needs GD to support!');
        
        // 若存在配置数组，则更改默认配置数组的值
        if (!empty($config) && is_array($config))
        {
            foreach ($config as $key => $value)
            {
                // 只有在默认配置数组中存在的键才更新其值
                if (array_key_exists($key, $this->_default))
                {
                    $this->_default[$key] = $value;
                }
            }
        }
        
        // 初始化各个属性的值
        foreach ($this->_default as $key => $value)
        {
            $property        = '_'.$key;
            $this->$property = $value;
        }
        
        // 若没设置折线、网格颜色，则默认使用字体的颜色
        empty($this->_brokenColor) ? $this->_brokenColor = $this->_color : null;
        empty($this->_gridColor)   ? $this->_gridColor   = $this->_color : null;
        
        // 若没设置字体，则使用默认字体
        if (empty($this->_font))
        {
            $this->_font = array(
                dirname(__FILE__).'/fonts/cantata.ttf',
                dirname(__FILE__).'/fonts/ledger.ttf'
            );
        }
        
        return $this;
    }
    
    /**
     * 创建图像资源[private]
     * 
     * 返回值[object]：对象本身
     */
    private function createImage()
    {
        $this->_image = imagecreatetruecolor($this->_width, $this->_height);
        
        // 图像资源创建失败则终止脚本继续执行
        $this->_image ? null : die('Fail to create image resource!');
        
        return $this;
    }
    
    /**
     * 填充图像背景颜色[private]
     * 
     * 返回值[object]：对象本身
     */
    private function fillBackground()
    {
        $color      = $this->transformToRgb($this->_background);
        $background = imagecolorallocate($this->_image, $color['r'], $color['g'], $color['b']);
        
        // 填充背景颜色
        imagefill($this->_image, 0, 0, $background);
        
        return $this;
    }
    
    /**
     * 随机产生字符[private]
     * 
     * 返回值[string]：随机产生的字符串
     */
    private function generateString(array $chars)
    {
        $maxIndex = count($chars) - 1;
        $string   = '';
        
        // 打乱数组值的顺序
        shuffle($chars);
        
        for ($i = 0; $i < $this->_length; ++$i)
        {
            $string .= $chars[mt_rand(0, $maxIndex)];
        }
        
        return $string;
    }
    
    /**
     * 随机产生验证码字符[private]
     * 
     * 返回值[object]：对象本身
     */
    private function generateCaptcha()
    {
        $letters = array_merge(range('a', 'z'), range('A', 'Z'));
        $numbers = range(0, 9);
        $both    = array_merge($letters, $numbers);
        
        switch ($this->_captchaType)
        {
            case self::LETTER:
                $this->_captcha = $this->generateString($letters);
                break;
                
            case self::NUMBER:
                $this->_captcha = $this->generateString($numbers);
                break;
                
            case self::BOTH:
            default:
                $this->_captcha = $this->generateString($both);
                break;
        }
        
        return $this;
    }
    
    /**
     * 绘画验证码字符到图像上[pirvate]
     * 
     * 返回值[object]：对象本身
     */
    private function drawCaptcha()
    {
        $color     = $this->transformToRgb($this->_color);
        $charWidth = intval(($this->_width - 2 * $this->_border) / $this->_length); 
        
        for ($i = 0; $i < $this->_length; ++$i)
        {
            $k         = mt_rand(0, count($color) - 1);
            $fontColor = imagecolorallocate($this->_image, $color[$k]['r'], $color[$k]['g'], $color[$k]['b']); 
            $font      = $this->_font[mt_rand(0, count($this->_font) - 1)];
            $fontSize  = mt_rand($this->_fontSize['min'], $this->_fontSize['max']);
            $angle     = mt_rand($this->_angle['min'], $this->_angle['max']);
            
            $measure = imageftbbox($fontSize, $angle, $font, $this->_captcha[$i]);
            $x       = $charWidth / $this->_length + $charWidth * $i;
            $y       = $this->_height / 2 + $this->_border + ($measure[2] - $measure[5]) / 4;
            
            imagefttext($this->_image, $fontSize, $angle, $x, $y, $fontColor, $font, $this->_captcha[$i]);
        }
        
        return $this;
    }
    
    /**
     * 绘画干扰素[private]
     * 
     * 返回值[object]：对象本身
     */
    private function drawLines()
    {   
        switch($this->_lineType)
        {
            case self::BROKEN:
                $color = $this->transformToRgb($this->_brokenColor);
                
                for ($i = 0; $i < $this->_lineCount; ++$i)
                {
                    $k         = mt_rand(0, count($color) - 1);
                    $lineColor = imagecolorallocatealpha($this->_image, $color[$k]['r'], $color[$k]['g'], $color[$k]['b'], $this->_transparent);
                    
                    $x1 = $this->_border;
                    $y1 = mt_rand($this->_border, $this->_height - $this->_border - 1);
                    $x2 = mt_rand($this->_border, $this->_width - $this->_border - 1);
                    $y2 = mt_rand($this->_border, $this->_height - $this->_border - 1);
                    $x3 = $this->_width - $this->_border - 1;
                    $y3 = mt_rand($this->_border, $this->_height - $this->_border - 1);
                    
                    imageline($this->_image, $x1, $y1, $x2, $y2, $lineColor);
                    imageline($this->_image, $x2, $y2, $x3, $y3, $lineColor);
                }
                
                break;
            
            case self::GRID:
            default:           
                $color = $this->transformToRgb($this->_gridColor);
                   
                for ($i = $this->_border + $this->_gridLength - 1; $i < $this->_height - $this->_border; $i += $this->_gridLength)
                {
                    $k         = mt_rand(0, count($color) - 1);
                    $lineColor = imagecolorallocatealpha($this->_image, $color[$k]['r'], $color[$k]['g'], $color[$k]['b'], $this->_transparent);
                    
                    imageline($this->_image, $this->_border, $i, $this->_width - $this->_border - 1, $i, $lineColor);
                }
                
                for ($i = $this->_border + $this->_gridLength - 1; $i < $this->_width - $this->_border; $i += $this->_gridLength)
                {
                    $k         = mt_rand(0, count($color) - 1);
                    $lineColor = imagecolorallocatealpha($this->_image, $color[$k]['r'], $color[$k]['g'], $color[$k]['b'], $this->_transparent);
                    
                    imageline($this->_image, $i, $this->_border, $i, $this->_height - $this->_border - 1, $lineColor);
                }
                
                break;
        }
        
        return $this;
    }
    
    /**
     * 绘画图像边框[private]
     * 
     * 返回值[object]：对象本身
     */
    private function drawBorder()
    {
        $color  = $this->transformToRgb($this->_borderColor);
        $border = imagecolorallocate($this->_image, $color['r'], $color['g'], $color['b']);
        
        for ($i = 0; $i < $this->_border; ++$i)
        {
            $x1 = $i;
            $y1 = $i;
            $x2 = $this->_width - $i - 1;
            $y2 = $this->_height - $i - 1;
            
            imagerectangle($this->_image, $x1, $y1, $x2, $y2, $border);
        }
        
        return $this;
    }
    
    /**
     * 输出图像[private]
     * 
     * 返回值[object]：对象本身
     */
    private function outputImage()
    {
        switch ($this->_imageType)
        {
            case self::GIF:
                header('content-type: image/gif');
                imagegif($this->_image);               
                break;
                
            case self::JPEG:
                header('content-type: image/jpeg');
                imagejpeg($this->_image);
                break;
                
            case self::PNG:
            default:
                header('content-type: image/png');
                imagepng($this->_image);                
                break;
        }
        
        return $this;
    }
    
    /**
     * 设置session[private]
     * 
     * 返回值[void]
     */
    private function setSession()
    {
        session_start();
        $_SESSION[$this->_sessionId] = strtolower($this->_captcha);
    }
    
    /**
     * 把十六进制颜色值转换为RGB[array]
     * 
     * $color[mixed]：颜色值的形式为'#ffffff'或'ffffff'
     *                可以是字符串或一维数组
     * 
     * 返回值[array]：返回转换为RGB格式的数组
     */
    private function transformToRgb($color)
    {
        if (is_array($color))
        {
            foreach ($color as $key => $value)
            {
                $color[$key] = str_replace('#', '', $value);
                
                $rgb[$key]['r'] = hexdec($color[$key][0].$color[$key][1]);
                $rgb[$key]['g'] = hexdec($color[$key][2].$color[$key][3]);
                $rgb[$key]['b'] = hexdec($color[$key][4].$color[$key][5]);
            }
        }
        else
        {
            $color = str_replace('#', '', $color);
            
            $rgb['r'] = hexdec($color[0].$color[1]);
            $rgb['g'] = hexdec($color[2].$color[3]);
            $rgb['b'] = hexdec($color[4].$color[5]);
        }
        
        return $rgb;
    }
    
    /**
     * 获取程序版本号[public:static]
     * 
     * 返回值[string]：返回程序版本号
     */
    public static function getVersion()
    {
        return self::$_version;
    }
    
    /**
     * 析构方法[public]
     * 
     * 释放创建的图像资源
     * 
     * 返回值[void]
     */
    public function __destruct()
    {
        $this->_image ? imagedestroy($this->_image) : null;
    }
}
